上篇文章講到實作 Semantic Kernel 前必須要擁有自己的 Open AI 的 API Key 才可以正常使用功能,這篇文章會講解官方附上的程式碼個別執行的工作
首先是 Semantic Kernel 的 SDK,目前最新的版本是 1.3.0
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-bom</artifactId>
<version>${sk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-aiservices-openai</artifactId>
</dependency>
</dependencies>
這裡的寫法會跟官方文件不太一樣,這是因為官方範例是使用 Azure Open AI 當作語意辨識的 AI,但是我們使用的是 Chat GPT,而正常來說 Open AI 可以自動將 endpoint 引入正確的網址,因此這邊要將 endpoint()
給移除
接著可能會對 .credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
這個部分感到疑惑,既然都不是使用 Azure Open AI 了,為什麼還是使用 AzureKeyCredential 當作 API Key 的憑證呢?
關於這個問題簡單來說就是 Microsoft 並沒有額外添加 Open AI 的 Credential,只要使用同樣的 AzureKeyCredential 並填入自己的 Open AI API Key 就可以使用了
最後 MODEL_ID 就是填入 Open AI 目前釋出可以使用的模型,這次的專案中都是使用 gpt-4o-mini
模型
OpenAIAsyncClient client = new OpenAIClientBuilder()
.credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
.buildAsyncClient();
// Create your AI service client
ChatCompletionService chatCompletionService = OpenAIChatCompletion.builder()
.withModelId(MODEL_ID)
.withOpenAIAsyncClient(client)
.build();
在這個部分將自定義的 plugin 包裝成 Kernel 可以處理的結構,接著將這個 plugin 丟給 Kernel 註冊,讓 AI 可以辨識有這個功能可以使用
// Import the LightsPlugin
KernelPlugin lightPlugin = KernelPluginFactory.createFromObject(new LightsPlugin(),
"LightsPlugin");
在這裡我們主要在做的事,是將自定義的功能加進 Kernel,讓我們的 AI 可以成功辨識需要執行的功能
當我們將自定義的功能寫好後,需要先將自定義的功能包裝好,接著需要添加到Kernel中,在官方範例中是建立了一個跟燈光有關的功能,稍後會介紹大概的寫法
最後的 InvocationContext,這個指定了每次回傳時的內容是只回最後一句
// Create a kernel with Azure OpenAI chat completion and plugin
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, chatCompletionService)
.withPlugin(lightPlugin)
.build();
// Add a converter to the kernel to show it how to serialise LightModel objects into a prompt
ContextVariableTypes
.addGlobalConverter(
ContextVariableTypeConverter.builder(LightModel.class)
.toPromptString(new Gson()::toJson)
.build());
// Enable planning
InvocationContext invocationContext = new InvocationContext.Builder()
.withReturnMode(InvocationReturnMode.LAST_MESSAGE_ONLY)
.withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
.build();
最後這一大段都是啟用的方法,其中比較重要的有
ChatHistory 代表的是這個對話的紀錄,也就是上下文的意思,可以選擇每次詢問 AI 後都會將詢問內容跟回話保存,套用到下次的對話中
results 代表的是回傳的內容,在這裡其實就是將跟 AI 溝通的部分變成一個 Function,並且設定這個對話的歷史紀錄、可以辨識的 kernel功能、回傳的內容,如此一來就省去自己寫 API 路徑,以及設定傳入的參數和接回傳內容的架構等等的功夫
接著就是針對這個 result 再 print 出回傳的文字就大功告成了 !
// Create a history to store the conversation
ChatHistory history = new ChatHistory();
// Initiate a back-and-forth chat
Scanner scanner = new Scanner(System.in);
String userInput;
do {
// Collect user input
System.out.print("User > ");
userInput = scanner.nextLine();
// Add user input
history.addUserMessage(userInput);
// Prompt AI for response to users input
List<ChatMessageContent<?>> results = chatCompletionService
.getChatMessageContentsAsync(history, kernel, invocationContext)
.block();
for (ChatMessageContent<?> result : results) {
// Print the results
if (result.getAuthorRole() == AuthorRole.ASSISTANT && result.getContent() != null) {
System.out.println("Assistant > " + result);
}
// Add the message from the agent to the chat history
history.addMessage(result);
}
} while (userInput != null && !userInput.isEmpty());
我們要自定義一個功能的話,就要像這樣將功能的結構、功能寫好,基本寫法就向下方程式碼展示的一樣
其中比較重要的有
@DefineKernelFunction
這個註解標記了這個 plugin 的方法,給予名字以及描述,要注意的是描述的部分一定要盡可能的準確,才可以避免有判斷錯誤的問題發生
@KernelFunctionParameter
這個註解標記了這個 plugin 的方法需要的參數有什麼,這樣一來使用這個功能就需要將參數傳入,我們也就可以讓這個功能再進行更進階的判斷
public class LightsPlugin {
// Mock data for the lights
private final Map<Integer, LightModel> lights = new HashMap<>();
public LightsPlugin() {
lights.put(1, new LightModel(1, "Table Lamp", false));
lights.put(2, new LightModel(2, "Porch light", false));
lights.put(3, new LightModel(3, "Chandelier", true));
}
@DefineKernelFunction(name = "get_lights", description = "Gets a list of lights and their current state")
public List<LightModel> getLights() {
System.out.println("Getting lights");
return new ArrayList<>(lights.values());
}
@DefineKernelFunction(name = "change_state", description = "Changes the state of the light")
public LightModel changeState(
@KernelFunctionParameter(name = "id", description = "The ID of the light to change") int id,
@KernelFunctionParameter(name = "isOn", description = "The new state of the light") boolean isOn) {
System.out.println("Changing light " + id + " " + isOn);
if (!lights.containsKey(id)) {
throw new IllegalArgumentException("Light not found");
}
lights.get(id).setIsOn(isOn);
return lights.get(id);
}
}